package com.hooper.dp.utils;

import cn.hutool.core.lang.TypeReference;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.hooper.dp.dto.RedisData;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.*;
import java.util.function.Function;

import static com.hooper.dp.constant.CommonConstant.COMMON_TTL;
import static com.hooper.dp.constant.ShopConstant.SHOP_CACHE_TTL;

/**
 * @author Tim Hooper
 * @version 1.0
 * @time 2023/02/26/14:31
 */
@Component
public class RedisClient {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private static final ExecutorService CACHE_SHOP_EXECUTOR =
            Executors.newFixedThreadPool(10);
    /**
     * 将数据转为json并保存到数据库中
     * @param key   key
     * @param data 数据
     * @param ttl  过期时间，单位：分钟
     */
    public void save2Redis(String key, Object data, long ttl) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(data), ttl, TimeUnit.MINUTES);
    }

    /**
     * 将热点数据保存到redis中永不过期，并设置逻辑过期时间
     * @param key  缓存的key
     * @param data 缓存数据
     * @param ttl  过期时间，单位：分钟
     */
    public void saveWithLogicalExpire(String key, Object data, long ttl) {
        com.hooper.dp.dto.RedisData<Object> redisData = new com.hooper.dp.dto.RedisData<>();
        redisData.setData(data);
        redisData.setExpireTime(LocalDateTime.now().plusMinutes(ttl));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    public <R, ID> R getPreventCachePassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbCallback) {
        String key = keyPrefix + id;
        String jsonResult = stringRedisTemplate.opsForValue().get(key);
        //不为空直接转为对象返回
        if (!StringUtils.isBlank(jsonResult)) {
            return JSONUtil.toBean(jsonResult, type);
        }
        //调用数据库查询
        R data = dbCallback.apply(id);
        if (data == null) {
            //若缓存和数据库都没有该数据，存入空值
            stringRedisTemplate.opsForValue().set(key, "", COMMON_TTL, TimeUnit.SECONDS);
        }
        //将数据存入缓存后返回
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(data),
                SHOP_CACHE_TTL, TimeUnit.MINUTES);
        return data;
    }

    public <R,ID> R getLogicalExpireData(String keyPrefix,ID id, Class<R> type,Function<ID,R> dbCallback ) {
        String key = keyPrefix + id;
        String jsonResult = stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.isBlank(jsonResult)) {
            return null;
        }
        com.hooper.dp.dto.RedisData<Object> redisData = JSONUtil.toBean(jsonResult,
                new TypeReference<com.hooper.dp.dto.RedisData<Object>>(){}.getType(), false);
        LocalDateTime expireTime = redisData.getExpireTime();
        //若逻辑实际未过期，直接返回数据
        if (expireTime.isAfter(LocalDateTime.now())){
            return JSONUtil.toBean((JSONObject) redisData.getData(),type);
        }
        //缓存数据
        RedisLockUtil redisLock = new RedisLockUtil(stringRedisTemplate, id+"");
        try {
            //获取锁
            if (redisLock.tryLock()) {
                //双检
                String result = stringRedisTemplate.opsForValue().get(key);
                com.hooper.dp.dto.RedisData doubleCheckData = JSONUtil.toBean(result, com.hooper.dp.dto.RedisData.class);
                if (doubleCheckData.getExpireTime().isAfter(LocalDateTime.now())){
                    return JSONUtil.toBean((JSONObject) redisData.getData(),type);
                }
                //第一个抢到锁的线程另开一个线程进行缓存操作
                CACHE_SHOP_EXECUTOR.submit(() -> {
                    com.hooper.dp.dto.RedisData<R> shopRedisData = new RedisData<>();
                    R data = dbCallback.apply(id);
                    shopRedisData.setData(data);
                    shopRedisData.setExpireTime(LocalDateTime.now().plusMinutes(COMMON_TTL));
                    stringRedisTemplate.opsForValue().set(key,
                            JSONUtil.toJsonStr(shopRedisData));
                });
            }
        } finally {
            redisLock.unlock();
        }
        return JSONUtil.toBean((JSONObject) redisData.getData(),type);
    }


}
